package com.jamal2362.apktool.task;

import android.content.Context;
import brut.androlib.Androlib;
import brut.androlib.AndrolibException;
import brut.androlib.ApkDecoder;
import brut.androlib.ApkOptions;
import brut.directory.ExtFile;
import brut.util.OS;
import com.google.common.collect.ImmutableList;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.logging.Level;
import org.jf.baksmali.Baksmali;
import org.jf.baksmali.BaksmaliOptions;
import org.jf.dexlib2.DexFileFactory;
import org.jf.dexlib2.analysis.ClassPath;
import org.jf.dexlib2.analysis.ClassPathResolver;
import org.jf.dexlib2.dexbacked.OatFile;
import org.jf.dexlib2.iface.DexFile;
import org.jf.dexlib2.iface.MultiDexContainer;
import org.xmlpull.v1.XmlPullParserException;
import com.jamal2362.apktool.R;
import com.jamal2362.apktool.fragment.files.Refreshable;
import com.jamal2362.apktool.util.FileUtils;
import com.jamal2362.apktool.util.Manifest;
import com.jamal2362.apktool.util.Settings;

public class DecodeTask extends AbstractTask
{
	private final String name;
	private static Context context;
	private static boolean antiSplit;
	private Refreshable refresh;
	private static File outputDir;
	private boolean decodeRes;
	private boolean decodeDex;

	public DecodeTask(Context ctx, Refreshable refresh, String name)
	{
		super(ctx, refresh);
		this.context = ctx;
		this.refresh = refresh;
		this.name = name;
		this.antiSplit = false;
		this.decodeRes = true;
		this.decodeDex = false;
	}
	
	public void setDecodeRes(boolean val)
	{
		this.decodeRes = val;
	}
	
	public void setDecodeDex(boolean val)
	{
		this.decodeDex = val;
	}

	@Override
	protected int getTitle()
	{
		return R.string.decode_run_title;
	}
	
	public void antiSplit(boolean b)
	{
		this.antiSplit = b;
	}

	@Override
	protected Boolean doInBackground(File[] p1)
	{
		return super.doInBackground(p1);
	}

	@Override
	protected boolean process(File f)
	{
		final ExtFile dir = getOutDir(f, name, this);
		int targetSdk = 14;
		try
		{
			Manifest mf = new Manifest(f);
			targetSdk = mf.getTargetSdkVersion();
		}
		catch (XmlPullParserException e)
		{}
		catch (IOException e)
		{}
		if (targetSdk < 1)
			targetSdk = 14;
		if (dir == null)
			return false;
		OS.rmdir(dir);
		dir.mkdirs();
		setResult(dir);
		ExtFile apk = new ExtFile(f);
		ApkOptions o = ApkOptions.INSTANCE;
		Androlib androLib = new Androlib(o, this);
		ApkDecoder decoder = new ApkDecoder(apk, androLib, this);
		decoder.setApkFileName(name);
		decoder.setBaksmaliDebugMode(Settings.getb(context, "mBakDeb", true));
		decoder.setDecodeAssets(ApkDecoder.DECODE_ASSETS_FULL);
		if (decodeRes)
			decoder.setForceDecodeManifest(ApkDecoder.FORCE_DECODE_MANIFEST_FULL);
		else
			decoder.setForceDecodeManifest(ApkDecoder.FORCE_DECODE_MANIFEST_NONE);
		if (decodeRes)
			decoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_FULL);
		else
			decoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_NONE);
		if (decodeDex)
			decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_SMALI);
		else
			decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_NONE);
		decoder.setOutDir(dir);
		decoder.setApi(targetSdk);
		decoder.setForceDelete(true);
		try
		{
			return decode(decoder);
		}
		finally
		{
			File smali = new File(dir, "smali");
			try
			{
				if (!smali.exists() && decodeDex)
					deodex(f, dir);
			}
			catch (Exception e)
			{
				log(Level.WARNING, "Deodex failed", e);
			}
		}
	}

	private void deodex(File f, File dir) throws Exception
	{
		info(R.string.text, "Find odex file...");
		File odex = find(f.getParentFile(), ".odex");
		if (odex == null)
			return;
		info(R.string.text, odex);
		InputStream i = new FileInputStream(odex);
		DexFileFactory.FilenameVdexProvider p = new DexFileFactory.FilenameVdexProvider(odex);
		OatFile oat = OatFile.fromInputStream(new BufferedInputStream(i), p);
		i.close();
		List<String> names = oat.getDexEntryNames();
		BaksmaliOptions o = new BaksmaliOptions();
		o.localsDirective = true;
		o.sequentialLabels = true;
		o.allowOdex = true;
		o.implicitReferences = true;
		o.deodex = true;
		for (int idx = 0; idx < names.size(); idx++)
		{
			String name = names.get(idx);
			DexFile dex = (DexFile) oat.getEntry(name);
			o.classPath = getClassPath(odex, dex, oat.getOatVersion());
			File d;
			if (idx == 0)
				d = new File(dir, "smali");
			else
				d = new File(dir, String.format("smali_class%d", idx + 1));
			d.mkdirs();
			info(R.string.text, String. format("Decode %s to %s", name, d));
			Baksmali.disassembleDexFile(dex, d, 5, o);
		}
	}

	private static ClassPath getClassPath(File path, DexFile dex, int version) throws IOException
	{
		String arch = path.getParentFile().getName();
		String cp = "/system/framework/" + arch;
		ImmutableList<String> boot = ImmutableList.of(cp);//, "/system/framework/oat");
		ImmutableList.Builder<String> entries = new ImmutableList.Builder<>();
		for (File f : new File(cp).listFiles())
		{
			String name = f.getName();
			if (name.endsWith(".oat"))
				entries.add(name);
		}
		ClassPathResolver resolver = new ClassPathResolver(
			boot,
			entries.build(),
			ImmutableList.<String>of(),
				(MultiDexContainer.DexEntry<?>) dex);
		return new ClassPath(resolver.getResolvedClassProviders(), true,
							 version);
	}

	private File find(File f, String ext)
	{
		if (f.isDirectory())
		{
			for (File f1 : f.listFiles())
			{
				f1 = find(f1 , ext);
				if (f1 != null)
					return f1;
			}
		}
		else if (f.getName().endsWith(ext))
			return f;
		return null;
	}

	private boolean decode(ApkDecoder decoder)
	{
		try
		{
			decoder.decode();
			return true;
		}
		catch (AndrolibException e)
		{
			log(Level.WARNING, "Decompile failed", e);
		}
		return false;
	}

	public static ExtFile getOutDir(File f, String name, AbstractTask task)
	{
		String dir = f.getParent();
		if (name == null)
			name = f.getName();
		int e = name.lastIndexOf('.');
		if (e >= 0)
			name = name.substring(0, e);
		boolean overwrite = Settings.getb(task.getContext(), "overwrite_apk", true);
		boolean allToOut = Settings.getb(task.getContext(), "all_to_out_dir", false);
		boolean inst = FileUtils.isInstalledApk(task.getContext(), f);
		boolean noaccess = !f.getParentFile().canWrite();
		if ((allToOut || inst || noaccess) && !antiSplit)
		{
			dir = Settings.output_directory;
			if (dir == null)
			{
				task.warning(R.string.output_directory_not_set);
				return null;
			}
			File d =  new File(dir);
			if (!d.exists() && !d.mkdirs())
			{
				task.warning(R.string.output_directory_not_extsts, dir);
				return null;
			}
			if (!d.isDirectory())
			{
				task.warning(R.string.not_directory, dir);
				return null;
			}
		}
		outputDir = f.getParentFile();
		name = name + (antiSplit ? "" : "_src");
		if (!overwrite)
			name = FileUtils.genName(context, dir, name, "", 0);
		return new ExtFile(dir, name);
	}
	
	@Override
	protected boolean shouldShowFinishDialog()
	{
		return !this.antiSplit;
	}

	@Override
	protected void onPostExecute(Boolean result)
	{
		super.onPostExecute(result);
		if (this.antiSplit)
		{
			AntiSplit as = new AntiSplit(this.context, this.refresh);
			as.execute(this.outputDir);
		}
	}
}
